package controler.algorithms.knn;

import java.util.Arrays;
import java.util.LinkedList;

import controler.algorithms.Algorithm;
import controler.algorithms.KNNExtraction;

/** 
 * @file KNN.java  
 * 
 **/

/**
 * @class KNN
 *   K-najblizszych sasiadow
 * 
 *        Klasa udostepniajaca interface do przeprowadzenia predykcji metoda KNN. 
 *        
 * 
 * @param ARITHMETIC_MEAN
 *            Typ wyliczeniowy wykorzystywany jako argument w konstruktorze, sygnalizuje 
 *            ze predykowana wartosc ma byc wyliczana przy uzyciu sredniej arytmetycznej.
 *            
 * @param MEDIAN
 *            Typ wyliczeniowy wykorzystywany jako argument w konstruktorze, sygnalizuje 
 *            ze predykowana wartosc ma byc wyznaczana jako mediana.
 *            
 * @param WEIGHTED
 *            Typ wyliczeniowy wykorzystywany jako argument w konstruktorze, sygnalizuje 
 *            ze predykowana wartosc ma byc wyliczana przy uzyciu sredniej wazonej. Gdzie 
 *            kolejne wagi są odwrotnie proporcjonalne do ich dystansu od predykowanej wartosci.  
 *            
 * @param EUKLIDES
 *            Typ wyliczeniowy wykorzystywany jako argument w konstruktorze, sygnalizuje 
 *            ze odleglosc sasiadow ma byc wyznaczona wg metryki euklidesowej.
 *            
 * @param COSINUS
 *            Typ wyliczeniowy wykorzystywany jako argument w konstruktorze, sygnalizuje 
 *            ze odleglosc sasiadow ma byc wyznaczona wg metryki kosinusowej.
 *            
 * @param trainingSet
 *            Lista kolejnych wartosci typu Double, reprezentująca zbior treningowy.
 *            
 * @param testSet
 *            Lista kolejnych wartosci typu Double, reprezentująca zbiór testowy.
 *            
 * @param K           
 *            Ilosc rozpatrywanych sasiadow.
 *            
 * @param H
 * 			  Ilosc kolejnych pomiarow branych pod uwage w jednym sasiedzie.
 * 
 * @param P
 * 			  Ilosc predykowanych wartosci.
 * 
 * @param meanType
 * 			  Zmienna typu int, ktora okresla w jaki sposob ma byc wyzaczona wartosc predykowana.
 * 
 * @param metric
 * 			  Zmienna typu int, która okresla w jakiej metryce maja byc wyznaczani najblizsi sasiedzi.
 * 
 * @param neighbours
 * 			  Pomocnicza tablica przechowywujaca sasiadow. 
 *            
 */

public class KNN extends Algorithm {
	public final static int ARITHMETIC_MEAN = 0;
	public final static int MEDIAN = 1;
	public final static int WEIGHTED = 2;
	public final static int EUKLIDES = 3;
	public final static int COSINUS = 4;

	private LinkedList<Double> trainingSet;
	private LinkedList<Double> testSet;

	private final int K;
	private final int H;
	private final int P = 1;
	private int meanType;
	private int metric;
	private Neighbour[] neighbours;

	
	/**
	 *   Konstruktor.
	 * 
	 *        Konstruktor, ktory przyjmuje jako argumenty k - ilosc sasiadow branych pod uwagę, 
	 *        h - ilosc pomiarow w sasiedzie, meanType - sposob wyznaczania wartosci predykowanej,
	 *        metric - metryka w jakiej liczone beda odleglosci miedzy sasiadami. 
	 */
	
	public KNN(int k, int h, int meanType, int metric) {
		super.setExtraction(new KNNExtraction());
		super.setRecursionDepth(h);
		K = k;
		H = h;
		this.meanType = meanType;
		this.metric = metric;
	}

	public LinkedList<Double> getTrainingSet() {
		return trainingSet;
	}
	
	/**
	 *   Metoda przetwarzajaca dane.
	 * 
	 *        Dane sa dzielone na wszystkich sasiadow po czym sa rozlokowywane w tablicy.
	 */

	public void setTrainingSet(LinkedList<Double> trainingSet) {
		this.trainingSet = trainingSet;

		System.out.println(trainingSet.size());
		int neighboursNumber = trainingSet.size() - H;

		neighbours = new Neighbour[neighboursNumber];

		for (int i = 0; i < neighboursNumber; i++) {
			double[] values = new double[H];
			for (int j = i; j < (H + i); j++) {
				values[j - i] = trainingSet.get(j);
			}
			neighbours[i] = new Neighbour();
			neighbours[i].setFeatures(values);
			neighbours[i].setP(trainingSet.get(i + H));
//			System.out.println(neighbours[i].toString());
		}
	}

//	public double predict(int t) {
//		Neighbour predictedNeighbour = new Neighbour();
//		double[] hSet = new double[H];
//		if (t >= H && t < testSet.size()) {
//			for (int i = 0; i < H; i++) {
//				hSet[i] = testSet.get(t + i - H);
//			}
//		} else System.out.println("Wyszedłem za zakres");
//		predictedNeighbour.setFeatures(hSet);
////		System.out.println(predictedNeighbour.toString());
//		double[] KNN = null;
//		switch (metric) {
//		case 3: 
//			KNN = findKNNEuklides(hSet);
//			break;
//		case 4:
//			KNN = findKNNCos(hSet);
//		}
//		return computePredictedValue(KNN, meanType);
//	}
	
	/**
	 *   Metoda wyznaczajaca wartosc przewidywana.
	 * 
	 *        W pierwszej kolejnosci  okreslana zostaje metryka, nastepnie w niej szukamy
	 *        k-najblizszych sasiadow, na podstawie czego jest wyznaczana wartosc predykowana.
	 */
	
	public double predict2(LinkedList<Double> ts) {
		double[] hSet = new double[ts.size()];
		for (int i =0; i < ts.size(); i++) {
			hSet[i] = ts.get(i);
		}
		double[] KNN = null;
		switch (metric) {
		case 3: 
			KNN = findKNNEuklides(hSet);
			break;
		case 4:
			KNN = findKNNCos(hSet);
		}
		return computePredictedValue(KNN, meanType);
	}
	
	/**
	 *   Metoda liczaca nowa wartosc.
	 * 
	 *        Wartosc moze byc wyznaczona poprzez srednia arytmetyczną, wazona lub jako mediana wartosci.
	 *        W sredniej wazonej, wagi sa odwrotnie proporcjonalne od odlegsosci od punktu centralnego.
	 */

	private double computePredictedValue(double[] KNN, int meanType) {
		double avg = 0d;
		switch (meanType) {
		case 0:
			for (int i = 0; i < KNN.length; i++) {
				avg += KNN[i];
			}
			avg /= KNN.length;
			break;
		case 1:
			avg = KNN[KNN.length / 2];
			break;
		case 2:
			double weightSum = 0d;
			double maxDistance = KNN[KNN.length-1];
			
			for (int i = 0; i < KNN.length; i++) {
				avg += maxDistance;
				weightSum += maxDistance/KNN[i];
			}
			avg = avg / weightSum;
		default:
			break;
		}
		return avg;
	}

	/**
	 *   KNN - metryka euklidesowa.
	 * 
	 *        W metryce euklidesowej odlegsosci sa liczone analogicznie do dlugosci wektora w przestrzenii.
	 */
	
	private double[] findKNNEuklides(double[] point) {
		double[] pNNSet = new double[K];

		for (int i = 0; i < neighbours.length; i++) {
			countEuclideanDistance(neighbours[i], point);
		}

		Arrays.sort(neighbours);
		for (int i = 0; i < K; i++) {
			pNNSet[i] = neighbours[i].getP();
		}
//		 System.out.println(neighbours.length);
//		 for(int i = 0; i < neighbours.length; i++) {
//		 System.out.println(neighbours[i].getDistance());
//		 }

		return pNNSet;
	}
	
	/**
	 *   KNN - metryka kosinusowa.
	 * 
	 *        Odleglosc w metryce kosinusowej, wg. wzoru.
	 */
	
	private double[] findKNNCos(double[] point) {
		double[] pNNSet = new double[K];
		for (int i = 0; i < neighbours.length; i++) {
			countCosDistance(neighbours[i], point);
		}

		Arrays.sort(neighbours);
		for (int i = 0; i < K; i++) {
			pNNSet[i] = neighbours[i].getP();
		}
		
		return pNNSet;
	}

	private void countEuclideanDistance(Neighbour neighbour, double[] point) {
		double sum = 0d;
		double[] values = neighbour.getFeatures();
		for (int i = 0; i < point.length; i++) {
			sum += Math.pow((values[i] - point[i]), 2);
		}
		neighbour.setDistance(Math.sqrt(sum));
	}
	
	private void countCosDistance(Neighbour neighbour, double[] point) {
		double sum = 0d;
		double powX = 0d;
		double powY = 0d;
		double[] values = neighbour.getFeatures();
		for (int i = 0; i < point.length; i++) {
			sum += values[i] * point[i];
			powX += Math.pow(values[i], 2);
			powY += Math.pow(point[i], 2);
		}
		
		powX = Math.sqrt(powX);
		powY = Math.sqrt(powY);
		
		if ((powX * powY) != 0d) 
			neighbour.setDistance(1 - sum/(powX*powY));
		else {
			System.out.println("Nie policzylem odleglosci punktu w metryce cosinusowej");
			neighbour.setDistance(0d);
		}
	}

	public LinkedList<Double> getTestSet() {
		return testSet;
	}

	public void setTestSet(LinkedList<Double> testSet) {
		this.testSet = testSet;
	}

	/**
	 *   Uczenie algorytmu.
	 * 
	 *       Polega tylko na pobraniu interesujacej nas wartosci (natezenie ruchu - 5-ty indeks tablicy) i dodaniu 
	 *       jej do zbioru uczącego. 
	 */
	
	@Override
	public boolean learn(LinkedList<double[]> trainingSet) {
		LinkedList<Double> ts = new LinkedList<Double>();
		for (int i = 0; i < trainingSet.size(); i++) {
			ts.add(new Double(trainingSet.get(i)[5]));
		}
		setTrainingSet(ts);
		return true;
	}
	

	@Override
	public double predict(LinkedList<double[]> dataSet) {
		LinkedList<Double> ts = new LinkedList<Double>();
		for (int i = 0; i < dataSet.size(); i++) {
			ts.add(new Double(dataSet.get(i)[5]));
		}
		setTestSet(ts);
		return predict2(ts);
	}

	
	public String toString() {
		String text = "---------------------\n";
		text += "KNN: k = "+K+", h = "+H;
		switch (meanType) {
		case 0: text += ", srednia arytmetyczna";break;
		case 1: text += ", mediana";break;
		case 2: text += ", srednia wazona";break;
		}
		
		switch (metric) {
		case 3: text += ", metryka euklidesowa";break;
		case 4: text += ", metryka kosinusowa";break;
		}
		return text;
	}

}
